# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# Name:        dir_cache.py
# Purpose:     GAEDrive Client: cache of directory and file meta-data
#
# Author:      Crane Chu <cranechu (at) gmail.org>
#
# Created:     Nov. 20, 2009
# Copyright:   Copyright 2009,2010 Crane Chu
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#-------------------------------------------------------------------------------
#!/usr/bin/env python

__author__ = 'Crane Chu'


import cPickle
import errno
import os.path
import gaedrive_config
from time import time
from fuse import fuse_get_context
from stat import S_IFDIR, S_IFREG, S_IFLNK


DIR_FILE = '/.dir_data'


# define a named tuple
class iNode(object):
    def __init__(self, name, stat, chunks, xattr, subfiles):
        self.name = name
        self.stat = stat
        self.xattr = xattr
        self.subfiles = subfiles
        self.chunks = chunks
        self.symlink = None
        self.parent = None
        
    def __getattr__(self, name):
        if name in ['name', 'stat', 'xattr', 'subfiles', 'chunks']:
            return self.__dict__[name]
        else:
            return super(iNode, self).__getattr__(name)

    def __setattr__(self, name, value):
        if name in ['name', 'stat', 'xattr', 'subfiles', 'chunks']:
            self.__dict__[name] = value
            return value
        else:
            return super(iNode, self).__setattr__(name, value)


class DIRCache(object):
    def __init__(self, net_io, cache_path = None):
        self.net_io = net_io
        self.cache_path = cache_path
        self.statfs_root = None

        # init the root dir tree
        ret = self._fetch_dir('')
        if ret and ret[0]:
            # has connected
            self.root = ret[0]
        else:
            # offline, read dir data from nvm
            try:
                with open(cache_path + DIR_FILE, 'r') as f:
                    self.root = cPickle.load(f)
            except IOError:
                # no such dir file
                self.root = None
        
    def _fetch_dir(self, path):
        """ get the dir data from master Server
        
        Args:
            path: path of the dir
        
        Returns:
            the list of file's iNode in the directory.
        """
        # update this dir info
        subfiles = self.net_io.rpc_master("readdir", path)
        parent_inode = self._get_inode(path) if path else None
        ret = []
        
        if subfiles is False or subfiles is None:
            # network disconnected
            return False
        
        for file in subfiles:
            inode = iNode(file[0], file[1], file[3], file[2], None)
            inode.parent = parent_inode
            inode.symlink = file[4]
            ret.append(inode)
            
        return ret
        
    def _get_inode(self, path):
        """ locate the inode within the dir tree
        
        Args:
            path: the path of file
            
        Returns:
            inode: (name, stat, xattr, subfiles, chunks)
        
        Raises:
            OSError(errno.ENOENT, ''): no i-node found
        """
        name = os.path.basename(path)
        if name != '':
            parent = self._get_inode(os.path.dirname(path))
            if parent.subfiles == None:
                # update the dir data
                parent.subfiles = self._fetch_dir(os.path.dirname(path))
                
            # find the inode in current dir subfiles
            for inode in parent.subfiles:
                if inode.name == name:
                    return inode
            else:
                # can not find the path, raise the error
                raise OSError(errno.ENOENT, '')
        else:
            # the path is root dir '/'
            return self.root

    def _gen_pathname(self, inode):
        names = []
        while inode:
            names.append(inode.name)
            inode = inode.parent
        names.reverse()

        return '/'.join(names)
    
    def readdir(self, dir):
        dir_inode = self._get_inode(dir)
        
        if dir_inode.subfiles == None:
            # update the dir data
            dir_inode.subfiles = self._fetch_dir(dir)
            
        return ['.', '..'] + \
                [file.name.encode('utf-8') for file in dir_inode.subfiles]

    def getattr(self, path):
        inode = self._get_inode(path)
        if inode.stat:
            # set the owner to current user
            uid, gid, pid = fuse_get_context(); pid
            inode.stat["st_uid"] = uid
            inode.stat["st_gid"] = gid
            return inode.stat
            
    def readlink(self, path):
        inode = self._get_inode(path)
        return inode.symlink
        
    def getxattr(self, path, name):
        inode = self._get_inode(path)
        return inode.xattr[name] if name in inode.xattr else ''
        
    def listxattr(self, path):
        inode = self._get_inode(path)
        return inode.xattr.keys()

    def statfs(self, path):
        if path == '/':
            if not self.statfs_root:
                self.statfs_root = self.net_io.rpc_master("statfs", path)
            ret = self.statfs_root
        else:
            ret = self.net_io.rpc_master("statfs", path)
            
        return ret

    def setxattr(self, path, name, value):
        ret = self.net_io.rpc_master("setxattr", path, name, value)
        if ret != False:
            inode = self._get_inode(path)
            inode.xattr[name] = value
            return True
        else:
            return False
        
    def removexattr(self, path, name):
        ret = self.net_io.rpc_master("removexattr", path, name)
        if ret != False:
            inode = self._get_inode(path)
            if name in inode.xattr:
                del inode.xattr[name]
                ret = True
            else:
                ret = False
                
        return ret

    def chmod(self, path, mode):
        ret = self.net_io.rpc_master("chmod", path, mode)
        if ret != False:
            inode = self._get_inode(path)
            inode.stat["st_mode"] &= 0o770000
            inode.stat["st_mode"] |= mode
            ret = True

        return ret

    def chown(self, path, uid, gid):
        ret = self.net_io.rpc_master("chown", path, uid, gid)
        if ret != False:
            inode = self._get_inode(path)
            inode.stat["st_uid"] = uid
            inode.stat["st_gid"] = gid
            ret = True

        return ret

    def utimens(self, path, times):
        ret = self.net_io.rpc_master("utimens", path, times)
        if ret != False:
            inode = self._get_inode(path)
            inode.stat["st_atime"] = ret[0]
            inode.stat["st_mtime"] = ret[1]
            ret = True
        
        return ret

    def create(self, path, mode):
        ret = self.net_io.rpc_master("create", path, mode)
        if ret != False:
            parent_path = os.path.dirname(path)
            parent_inode = self._get_inode(parent_path)
            st = dict(st_mode = (S_IFREG | int(mode)),
                        st_nlink = 1,
                        st_size = 0,
                        st_ctime = ret, # use time on master server
                        st_mtime = ret,
                        st_atime = ret,
                        st_uid = 0,
                        st_gid = 0)
            if parent_inode.subfiles is None:
                # update the dir data
                parent_inode.subfiles = self._fetch_dir(parent_path)
            else:
                newinode = iNode(os.path.basename(path), st, [], {}, [])
                newinode.parent = parent_inode
                parent_inode.subfiles.append(newinode)
            parent_inode.stat["st_nlink"] += 1
            
            ret = True
        else:
            ret = False
        
        return ret

    def mkdir(self, path, mode):
        ret = self.net_io.rpc_master("mkdir", path, mode)
        if ret != False:
            parent_path = os.path.dirname(path)
            parent_inode = self._get_inode(parent_path)
            st = dict(st_mode = (S_IFDIR | int(mode)),
                        st_nlink = 2,
                        st_size = 0,
                        st_ctime = ret, # use time on master server
                        st_mtime = ret,
                        st_atime = ret,
                        st_uid = 0,
                        st_gid = 0)
            if parent_inode.subfiles is None:
                # update the dir data
                parent_inode.subfiles = self._fetch_dir(parent_path)
            else:
                newinode = iNode(os.path.basename(path), st, [], {}, [])
                newinode.parent = parent_inode
                parent_inode.subfiles.append(newinode)
            parent_inode.stat['st_nlink'] += 1
            ret = True
        
        return ret
    
    def symlink(self, target, source):
        ret = self.net_io.rpc_master("symlink", target, source)
        if ret != False:
            parent_path = os.path.dirname(target)
            parent_inode = self._get_inode(parent_path)
            st = dict(st_mode = (S_IFLNK | 0777),
                        st_nlink = 1,
                        st_size = len(source),
                        st_ctime = ret, # use time on master server
                        st_mtime = ret,
                        st_atime = ret,
                        st_uid = 0,
                        st_gid = 0)
            
            if parent_inode.subfiles is None:
                # update the dir data
                parent_inode.subfiles = self._fetch_dir(parent_path)
                
            # save the source in subfiles[0]
            newinode = iNode(os.path.basename(target), st, [], {}, [])
            newinode.parent = parent_inode
            newinode.symlink = source
            parent_inode.subfiles.append(newinode)
            parent_inode.stat["st_nlink"] += 1
        
        return ret
        
    def open(self, path, flags):
        ret = self.net_io.rpc_master("open", path, flags)
        if ret is not None and ret < 0:
            raise OSError(-ret, '')
        else:
            return ret

    def rename(self, old, new):
        ret = self.net_io.rpc_master("rename", old, new)
        if ret != False:
            oldparent_inode = self._get_inode(os.path.dirname(old))
            old_inode = self._get_inode(old)
            
            newparent_path = os.path.dirname(new)
            newparent_inode = self._get_inode(newparent_path)
            oldparent_inode.subfiles.remove(old_inode)
            if newparent_inode.subfiles is None:
                # update the dir data
                newparent_inode.subfiles = self._fetch_dir(newparent_path)
            newparent_inode.subfiles.append(old_inode)
            old_inode.name = os.path.basename(new)
            ret = True
        
        return ret

    def rmdir(self, path):
        parent_inode = self._get_inode(os.path.dirname(path))
        dir_inode = self._get_inode(path)
        
        # find and return all chunks
        chunks = []
        if dir_inode.subfiles is None:
            # update the dir data
            dir_inode.subfiles = \
                    self._fetch_dir(self._gen_pathname(dir_inode))
        files = dir_inode.subfiles
        for file in files:
            if file.subfiles is None:
                # update the dir data
                file.subfiles = \
                        self._fetch_dir(self._gen_pathname(file))
            files += file.subfiles
            if file.chunks is None:
                file.chunks = \
                        self.net_io.rpc_master("getchunks",
                                               self._gen_pathname(file), 0)
            chunks += file.chunks
            
        ret = self.net_io.rpc_master("rmdir", path)
        if ret:
            parent_inode.subfiles.remove(dir_inode)
            del dir_inode
            parent_inode.stat["st_nlink"] -= 1
            ret = chunks
            
        return ret

    def unlink(self, path):
        # update dir mate info
        parent_inode = self._get_inode(os.path.dirname(path))
        file_inode = self._get_inode(path)
        
        # find and return all chunks
        if file_inode.chunks is None:
            file_inode.chunks = self.net_io.rpc_master("getchunks", path, 0)
        chunks = file_inode.chunks
        
        ret = self.net_io.rpc_master("unlink", path)
        if ret:
            self.statfs_root = ret
            parent_inode.subfiles.remove(file_inode)
            del file_inode
            parent_inode.stat["st_nlink"] -= 1
            ret = chunks

        return ret

    def truncate(self, path, length, rand):
        ret = self.net_io.rpc_master("truncate", path, length, rand)
        if ret:
            self.statfs_root = ret
            # update dir mate info for confirmation of time
            inode = self._get_inode(path)
            inode.stat["st_size"] = length
            if inode.chunks is None:
                inode.chunks = self.net_io.rpc_master("getchunks", path, 0)
            ret = inode.chunks
            # remove totally deleted chunks
            start_chunk = length // gaedrive_config.CHUNK_SIZE + 1
            chunks = range(start_chunk, len(inode.chunks))
            chunks.reverse()
            for index in chunks:
                inode.chunks.pop(index)
            
        return ret
    
    def getchunks(self, path, rand):
        inode = self._get_inode(path)
        if inode.chunks is None:
            inode.chunks = self.net_io.rpc_master("getchunks", path, rand)
            
        return inode.chunks
    
    def release(self, path, rand):
        file_inode = self._get_inode(path)
        file_size = file_inode.stat["st_size"]
        file_atime = file_inode.stat["st_atime"]
        return self.net_io.rpc_master("release",
                                      path, file_size, file_atime, rand)

    def write(self, path, size, offset, rand):
        file = self._get_inode(path)
        chunk_num = offset // gaedrive_config.CHUNK_SIZE
        offset_in_chunk = offset % gaedrive_config.CHUNK_SIZE
        
        if not file.chunks or len(file.chunks) <= chunk_num:
            file.chunks, now, self.statfs_root = \
                    self.net_io.rpc_master("write", path, offset, size, rand)
        else:
            now = time()
            
        #update local dir cache
        file.stat["st_size"] = max(file.stat["st_size"], offset + size)
        file.stat["st_mtime"] = now
        file.stat["st_atime"] = now
        
        chunk = file.chunks[offset // gaedrive_config.CHUNK_SIZE]
        write_size = min(size, gaedrive_config.CHUNK_SIZE - offset_in_chunk)
        return chunk, write_size
    
    def read(self, path, size, offset):
        file = self._get_inode(path)
        offset_in_chunk = offset % gaedrive_config.CHUNK_SIZE
        if not file.chunks:
            ret = self.net_io.rpc_master("read", path, offset, size)
            if ret != False:
                file.chunks, now = ret
            else:
                assert False
        else:
            now = time()
            
        #update local dir cache
        file.stat["st_atime"] = now
        
        chunk_index = offset // gaedrive_config.CHUNK_SIZE
        if chunk_index < len(file.chunks):
            chunk = file.chunks[chunk_index]
            read_size = min(size, gaedrive_config.CHUNK_SIZE - offset_in_chunk)
            ret_chunk = chunk
            ret_size = read_size
        else:
            ret_chunk = None
            ret_size = 0
        
        return ret_chunk, ret_size

    def clear(self):
        # clear and re-init the dir cache
        ret = self._fetch_dir('')
        self.root = ret[0]

    def destroy(self):
        # umount: store dir data to NVM
        with open(self.cache_path + DIR_FILE, 'w') as f:
            cPickle.dump(self.root, f)
            
